# -*- coding: utf-8 -*-
"""
UTc! module: src.game

Purpose
=======
 Implements server-side game logic.
 
Legal
=====
 All code, unless otherwise indicated, is original, and subject to the
 terms of the GPLv2, which is provided in COPYING.
 
 (C) Neil Tallim, 2009
"""
import random
import re
import socket
import threading
import time

import communication
import data_box

_TEAM_SIZE = 16 #: The maximum number of players to allow on a team.
_PLAYER_CAP = 96 #: The maximum number of players to allow in a game.

_ACTV_REGEXP = re.compile(r"ACTV \w+") #: A protocol-regexp that catches player-now-active events.
_ALIV_REGEXP = re.compile(r"(ALIV) \w+ (\d+) -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+ -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+") #: A protocol-regexp that catches player-now-alive-change events.
_DEAD_REGEXP = re.compile(r"(DEAD) (\w+) (\w+) (\d+)") #: A protocol-regexp that catches player-now-dead-change events.
_SUCI_REGEXP = re.compile(r"(SUCI) \w+ (\d+)") #: A protocol-regexp that catches player-suicide-change events.
_PPOS_REGEXP = re.compile(r"(PPOS) \w+ \d+ -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+ -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+ -?\d+\.\d+,-?\d+\.\d+ -?\d+\.\d+ -?\d+\.\d+ -?\d+\.\d+ -?\d+\.\d+ -?\d+\.\d+ -?\d+\.\d+ -?\d+\.\d+ \d -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+") #: A protocol-regexp that catches player-position events.
_MPOS_REGEXP = re.compile(r"(MPOS) (\w+) (\d+) \d+ -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+ -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+ \d+\.\d+") #: A protocol-regexp that catches missile-position events.
_BNEW_REGEXP = re.compile(r"(BNEW) \w+ \d+ \w{2} \d+ -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+ -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+") #: A protocol-regexp that catches new-bolt events.
_MNEW_REGEXP = re.compile(r"MNEW \w+ \d+ \w+ \w{2} \d+ -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+ -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+") #: A protocol-regexp that catches new-missile events.
_DAMG_REGEXP = re.compile(r"(DAMG) (\w+) \w+ (\d+) \d \d+\.\d+,\d+\.\d+,\d+\.\d+") #: A protocol-regexp that catches player-damage events.
_BDEL_REGEXP = re.compile(r"(BDEL) \w+ \d+") #: A protocol-regexp that catches bolt-deletion events.
_MDEL_REGEXP = re.compile(r"MDEL \w+ \d+") #: A protocol-regexp that catches missile-deletion events.
_MXPL_REGEXP = re.compile(r"MXPL -?\d+\.\d+,-?\d+\.\d+,-?\d+\.\d+") #: A protocol-regexp that catches missile-explosion events.
_TEXT_REGEXP = re.compile(r"TEXT \w+ .+") #: A protocol-regexp that catches player-text events.
_REGEXP_HANDLING = (
 (_PPOS_REGEXP, True),
 (_MPOS_REGEXP, True),
 (_BNEW_REGEXP, True),
 (_MNEW_REGEXP, False),
 (_DAMG_REGEXP, True),
 (_BDEL_REGEXP, True),
 (_MDEL_REGEXP, False),
 (_MXPL_REGEXP, False),
 (_ALIV_REGEXP, True),
 (_DEAD_REGEXP, True),
 (_SUCI_REGEXP, True),
 (_TEXT_REGEXP, False),
 (_ACTV_REGEXP, False)
) #: In order of expected frequency. A value of True means that the server needs to process the line instead of just forwarding it.

class Game(threading.Thread):
	"""
	A class for managing the game component of a UTc! server.
	"""
	_alive = True #: False when the game has ended.
	_callback = None #: A function to be called when the game has ended.
	_can_drop_players = True #: False once the game has ended.
	_client_lock = None #: A lock used to facilitate communication between clients and game-space.
	_duration = None #: The number of minutes that this game will last.
	_map = None #: A collection of all spaces that comprise the game field.
	_player_client_map = None #: The mapping between player names and connections.
	_player_map = None #: The mapping between player names and game-space data.
	_respawn_time = None #: The number of seconds players must wait before respawning.
	_score_cap = None #: The number of points at which a winner should be declared.
	_server = None #: The network server associated with this game.
	_shields_threshold = None #: The amount of shielding a player must have before their shields will come back online.
	_start_time = None #: The time at which the game began.
	_teams = None #: A collection of teams to which players may belong.
	
	def __init__(self, port, duration, score, callback, respawn_time, shields_threshold, average_size, size_variance, sphere_cube_ratio, radius, dispersion):
		"""
		Sets up and starts the game server, initializing the game field and
		setting all six teams' spawn points relative to the central cube.
		
		@type port: int
		@param port: The port on which the game server will run.
		@type duration: int
		@param duration: The number of minutes this game will last.
		@type score: int
		@param score: The score at which this game will be won, if non-0.
		@type callback: callable
		@param callback: The function to invoke when this game has ended.
		@type respawn_time: float
		@param respawn_time: The number of seconds that must pass before a player
		    will respawn.
		@type shields_threshold: float
		@param shields_threshold: The percentage of shielding that must recover
		    before players will be able to restore their shields.
		@type average_size: float
		@param average_size: The average size, in metres, of obstacles in the
		    field.
		@type size_variance: float
		@param size_variance: The percentage of average size by which individual
		    obstacles may vary.
		@type sphere_cube_ratio: float
		@param sphere_cube_ratio: The percentage of obstacles that will be
		    spheres.
		@type radius: int
		@param radius: The number of layers of obstacles to create.
		@type dispersion: float
		@param dispersion: The distance between the centres of each obstacle, in
		    metres.
		
		@raise socket.error: If the specified port is already in use.
		"""
		threading.Thread.__init__(self)
		self.setDaemon(False)
		
		self._client_lock = threading.RLock()
		
		self._respawn_time = respawn_time
		self._shields_threshold = shields_threshold
		
		self._duration = duration
		self._score_cap = score
		self._callback = callback
		
		self._player_client_map = {}
		self._player_map = {}
		
		min_size = average_size * (1.0 - size_variance)
		max_size = average_size * (1.0 + size_variance)
		map = ["SPAC box 0,0,0 0,0,0 100,100,100 333,333,333"]
		
		def _buildSphere(x, y, z):
			return "SPAC sphere %i,%i,%i %i,%i,%i %i %i,%i,%i" % (
			 x, y, z,
			 random.uniform(0.0, 360.0), random.uniform(0.0, 360.0), random.uniform(0.0, 360.0),
			 random.uniform(min_size, max_size) / 2.0,
			 random.randint(0, 1000), random.randint(0, 1000), random.randint(0, 1000),
			)
		def _buildBox(x, y, z):
			return "SPAC box %i,%i,%i %i,%i,%i %i,%i,%i %i,%i,%i" % (
			 x, y, z,
			 random.uniform(0.0, 360.0), random.uniform(0.0, 360.0), random.uniform(0.0, 360.0),
			 random.uniform(min_size, max_size), random.uniform(min_size, max_size), random.uniform(min_size, max_size),
			 random.randint(0, 1000), random.randint(0, 1000), random.randint(0, 1000),
			)
		for i in range(1, radius):
			distance = i * dispersion
			if i % 2:
				for x in (-distance, distance):
					for y in (-distance, distance):
						for z in (-distance, distance):
							if random.random() < sphere_cube_ratio:
								map.append(_buildSphere(x, y, z))
							else:
								map.append(_buildBox(x, y, z))
			else:
				for x in (-distance, 0.0, distance):
					for y in (-distance, distance):
						z_list = (0.0,)
						if not x:
							z_list = (-distance, distance)
						for z in z_list:
							if random.random() < sphere_cube_ratio:
								map.append(_buildSphere(x, y, z))
							else:
								map.append(_buildBox(x, y, z))
		self._map = tuple(map)
		
		#Each team should start along one spawn-point line to avoid giving an advantage to those in the back.
		self._teams = (
		 _Team(tuple([(-125 - (75 * x), 0, 0) for x in range(16)])),
		 _Team(tuple([(125 + (75 * x), 0, 0) for x in range(16)])),
		 _Team(tuple([(0, -125 - (75 * y), 0) for y in range(16)])),
		 _Team(tuple([(0, 125 + (75 * y), 0) for y in range(16)])),
		 _Team(tuple([(0, 0, -125 - (75 * z)) for z in range(16)])),
		 _Team(tuple([(0, 0, 125 + (75 * z)) for z in range(16)]))
		)
		
		self._updateTeamRanking()
		
		self._server = GameServer(port, self)
		self._server.start()
		
	def addPlayer(self, player_name, player_team, player_colour, player_ship, player_bolts, player_bolts_modifiers, player_missiles, player_power_source):
		"""
		Adds a player to the game.
		
		@type player_name: basestring
		@param player_name: The name of the player to add.
		@type player_team: int
		@param player_team: The ID of the team to which the player will belong.
		@type player_colour: sequence(3)
		@param player_colour: The (r, g, b) values of this player, as integers
		    from 0-1000.
		@type player_ship: basestring
		@param player_ship: The ID of the player's ship.
		@type player_bolts: sequence
		@param player_bolts: A collection of bolt IDs, specifying the player's
		    loadout.
		@type player_bolts_modifiers: sequence
		@param player_bolts_modifiers: A collection of modifier IDs, specifying
		    the player's bolt modifiers.
		@type player_missiles: sequence
		@param player_missiles: A collection of missile IDs, specifying the
		    player's loadout.
		@type player_power_source: int
		@param player_power_source: A power source ID, specifying the type of
		    energy that powers the player's ship.
		
		@rtype: str|None
		@return: A string describing the reason why the player was rejected, or
		    None if the join was successful.
		"""
		self._client_lock.acquire()
		try:
			if len(self._player_map) >= _PLAYER_CAP:
				return "game is full"
				
			team = self._teams[player_team]
			if len(team.getMembers()) >= _TEAM_SIZE:
				return "team is full"
				
			spawn_point = team.getSpawnPoints()[-len(team.getMembers()) - 1]
			
			player = _Player(player_name, player_team, player_colour, player_ship, player_bolts, player_bolts_modifiers, player_missiles, player_power_source, spawn_point)
			self._player_map[player_name] = player
			self._pushEvent(player.describe())
			team.addMember(player)
			
			return None
		finally:
			self._client_lock.release()
			
	def connectPlayer(self, player_name, connection):
		"""
		Handles the process of completing a connection from a client to the game
		server.
		
		@type player_name: basestring
		@param player_name: The name of the player being connected.
		@type connection: L{ClientConnection}
		@param connection: The connection thread that manages communication with
		    this player.
		"""
		self._client_lock.acquire()
		try:
			self._player_client_map[player_name] = connection
			self._pushEvent("JOIN %s" % (player_name), player_name, True)
		finally:
			self._client_lock.release()
			
	def disconnectPlayer(self, player_name):
		"""
		Removes a player from this game.
		
		@type player_name: basestring
		@param player_name: The name of the player who is being removed.
		"""
		self._client_lock.acquire()
		try:
			if player_name in self._player_client_map:
				del self._player_client_map[player_name]
				if self._can_drop_players: #Don't discard player data when the game is over.
					self._dropPlayer(player_name)
				self._pushEvent("PART %s" % (player_name))
		finally:
			self._client_lock.release()
			
	def getPlayers(self):
		"""
		Returns a collection of all players who are connected to this game
		server.
		
		@rtype: sequence
		@return: A collection of L{_Player} objects.
		"""
		self._client_lock.acquire()
		try:
			return self._player_map.values()
		finally:
			self._client_lock.release()
			
	def getDuration(self):
		"""
		Returns the number of minutes this game will last.
		
		@rtype: int
		@return: The number of minutes this game will last.
		"""
		return self._duration
		
	def getMap(self):
		"""
		Returns the position of each obstacle in the field.
		
		@rtype: tuple
		@return: A collection of protocol-events that describe the position of
		    each obstacle in the field.
		"""
		return self._map
		
	def getSpawnPoints(self):
		"""
		Returns the position of each spawn-point.
		
		@rtype: tuple
		@return: A collection of protocol-events that describe the position of
		    each spawn-point.
		"""
		return self._spawn_points
		
	def getTeam(self, id):
		"""
		Returns a team-status object.
		
		@type id: int
		@param id: The ID of the team to retrieve.
		
		@rtype: L{_Team}
		@return: The team being requested.
		"""
		return self._teams[id]
		
	def getTeams(self):
		"""
		Returns all team-status objects.
		
		@rtype: tuple
		@return: All L{_Team} objects.
		"""
		return self._teams
		
	def getTime(self):
		"""
		Returns the current game-timestamp, expressed in milliseconds.
		"""
		return int((time.time() - self._start_time) * 1000.0)
		
	def getVariables(self):
		"""
		Returns the respawn time and shields threshold specified for this game.
		
		@rtype: tuple(2)
		@return: The respawn time and shields threshold specified for this game.
		"""
		return (self._respawn_time, self._shields_threshold)
		
	def hasPlayer(self, name):
		"""
		Indicates whether this game server has a player with the given name.
		
		@type name: basestring
		@param name: The name of the user being sought.
		
		@rtype: bool
		@return: True if the named user is present.
		"""
		self._client_lock.acquire()
		try:
			return name in self._player_map
		finally:
			self._client_lock.release()
			
	def isAlive(self):
		"""
		Indicates whether this game server is active.
		
		@rtype: bool
		@return: True if this game server is still active.
		"""
		return self._alive
		
	def kill(self):
		"""
		Indicates that this lobby server should terminate gracefully.
		"""
		self._alive = False
		
	def processLine(self, line, player_name):
		"""
		Interprets data from a client, applying protocol regexps to determine how
		to react.
		
		@type line: basestring
		@param line: The line to process.
		@type player_name: basestring
		@param player_name: The name of the player who generated this line.
		"""
		for (regexp, process) in _REGEXP_HANDLING:
			match = regexp.match(line)
			if match:
				if not process: #Just forward it.
					self._pushEvent(line, player_name, True)
					break
				else: #Determine the nature of the event and deal with it.
					nature = match.group(1)
					self._client_lock.acquire()
					try:
						if nature == "PPOS":
							for (name, connection) in self._player_client_map.iteritems():
								if name == player_name:
									continue
								connection.pushPlayerEvent(player_name, line)
						elif nature == "MPOS":
							name = match.group(2)
							id = int(match.group(3))
							for (name, connection) in self._player_client_map.iteritems():
								if name == player_name:
									continue
								connection.pushMissileEvent(name, id, line)
						elif nature == "BNEW":
							player = self._player_map.get(player_name)
							if player:
								player.fireBolt()
							self._pushEvent(line, player_name, True)
						elif nature == "BDEL":
							player = self._player_map.get(player_name)
							if player:
								player.landBolt()
							self._pushEvent(line, player_name, True)
						elif nature == "DAMG":
							victim_name = match.group(2)
							victim = self._player_map.get(victim_name)
							if victim and victim.isAlive() and victim.getLastAlive() < int(match.group(3)):
								self._pushEvent(line, victim_name, False) 
						elif nature == "ALIV":
							player = self._player_map.get(player_name)
							if player and not player.isAlive():
								player.revive(int(match.group(2)))
								self._pushEvent(line, player_name, True)
						elif nature == "DEAD":
							victim_name = match.group(2)
							killer_name = match.group(3)
							victim = self._player_map.get(victim_name)
							if victim and victim.isAlive() and victim.getLastAlive() < int(match.group(4)):
								victim.kill()
								self._pushEvent("KILL %s %s" % (victim_name, killer_name))
								killer = self._player_map.get(killer_name)
								killer.addKill()
								
								score_modifier = 1
								if victim.getTeamID() == killer.getTeamID(): #Friendly kill.
									score_modifier = -1
								if self._teams[killer.getTeamID()].setScore(score_modifier) >= self._score_cap and self._score_cap: #Winning kill made.
									self.kill()
								self._updateTeamRanking()
						elif nature == "SUCI":
							player = self._player_map.get(player_name)
							if player and player.isAlive() and player.getLastAlive() < int(match.group(2)):
								player.kill(True)
								self._pushEvent("SUCI %s" % (player_name), player_name, True)
								self._teams[player.getTeamID()].setScore(-1)
								self._updateTeamRanking()
					finally:
						self._client_lock.release()
					break
					
	def run(self):
		"""
		Keeps this game server running until the game is interrupted by the score
		target being reached, a user-originated kill event, or the completion of
		its specified run-time.
		"""
		self._start_time = time.time()
		end_time = self._start_time + self._duration * 60.0
		
		while self._alive:
			time.sleep(1.0)
			if time.time() >= end_time:
				break
				
		self._server.kill()
		self._can_drop_players = False
		self._pushEvent("GEND")
		time.sleep(1.0) #Allow time for GEND to propagate.
		for connection in self._player_client_map.values():
			connection.kill()
		self._callback()
		
	def validatePlayer(self, player_name):
		"""
		Determines whether the named player is known to this server.
		
		@type player_name: basestring
		@param player_name: The name of the player being sought.
		
		@rtype: L{_Player}|None
		@return: The player, if found, or None if the player was not found.
		"""
		self._client_lock.acquire()
		try:
			player = self._player_map.get(player_name)
			if player and not player_name in self._player_client_map:
				return player
			return None
		finally:
			self._client_lock.release()
			
	def _dropPlayer(self, player_name):
		"""
		Performs the practical process of removing a player from this game
		server, if the player was actually present to begin with.
		
		@type player_name: basestring
		@param player_name: The name of the player to be removed.
		"""
		self._client_lock.acquire()
		player = self._player_map.get(player_name)
		if player:
			del self._player_map[player_name]
			self._teams[player.getTeamID()].removeMember(player)
		self._client_lock.release()
		
	def _pushEvent(self, event, player_filter=None, exclude_named_player=False):
		"""
		Propagates an event to a subset of all connected clients.
		
		@type event: basestring
		@param event: The event to send.
		@type player_filter: basestring|None
		@param player_filter: The name of the player to treat specially, or None
		    if all players should receive this message.
		@type exclude_named_player: bool
		@param exclude_named_player: True if the named player should not receive
		    this event; False if B{only} the named player should receive it.
		"""
		self._client_lock.acquire()
		try:
			if not player_filter:
				for (name, connection) in self._player_client_map.iteritems():
					connection.pushEvent(event)
			else:
				for (name, connection) in self._player_client_map.iteritems():
					if exclude_named_player:
						if name == player_filter:
							continue
						connection.pushEvent(event)
					elif not exclude_named_player and name == player_filter:
						connection.pushEvent(event)
						break
		finally:
			self._client_lock.release()
			
	def _updateTeamRanking(self):
		"""
		Recomputes the rank of each team relative to every other team, based on
		score. The computed rank order is then applied to each team's status
		object.
		"""
		self._client_lock.acquire()
		try:
			for (i, (score, id)) in enumerate(reversed(sorted([(team.getScore(), id) for (id, team) in enumerate(self._teams)]))):
				self._teams[id].setRank(i + 1)
				
			teams = self._teams
			player_map = self._player_map
			for (name, connection) in self._player_client_map.iteritems():
				connection.pushEvent("RANK %i %i" % teams[player_map[name].getTeamID()].getStatus())
		finally:
			self._client_lock.release()
			
			
class GameServer(communication.ServerSocket):
	"""
	A server process that continually listens for and accepts connections from
	would-be clients.
	"""
	def __init__(self, port, game):
		communication.ServerSocket.__init__(self, port, ClientConnection, (game,))
		
		
class ClientConnection(threading.Thread):
	"""
	A connection from a client.
	"""
	_alive = True #: False when this thread is ready to be killed.
	_client_socket = None #: The connection to the client.
	_data_box = None #: A container for data remaining to be processed.
	_game = None #: The game with which the client interacts.
	_lock = None #: A lock to keep the event queue thread-safe.
	_missiles_updates = None #: A list of all missile position updates waiting to be pushed to the player.
	_player = None #: The _Player object of the player this thread manages.
	_player_name = None #: The name of the player this connection handles.
	_players_updates = None #: A list of all player position updates waiting to be pushed to the player.
	_queue = None #: A list of messages to send to the player.
	_throttle = None #: The rate at which this connection should be polled for events.
	
	def __init__(self, client, args):
		"""
		Performs a handshake with the would-be client before accepting the
		connection and exposing it to the server.
		
		@type client: C{socket.socket}
		@param client: The socket by which connection with the client is made
		    possible.
		@type args: sequence
		@param args: The lobby server with which this client will interact.
		"""
		threading.Thread.__init__(self)
		self.setDaemon(False)
		
		(game,) = args
		
		self._lock = threading.RLock()
		self._queue = []
		self._game = game
		self._data_box = data_box.DataBox()
		self._client_socket = client_socket = communication.ClientSocket(client, self._data_box)
		self._missiles_updates = {}
		self._players_updates = {}
		
		try:
			self._client_socket.readData(True)
		except: #No harm in failing at this point.
			self.kill()
			return
		player_name = self._data_box.getLine()
		try:
			if not game.isAlive():
				self._client_socket.sendData("FAIL game has ended")
				self.kill()
			else:
				self._player = game.validatePlayer(player_name)
				if not self._player:
					self._client_socket.sendData("FAIL not authorized")
					self.kill()
				else:
					player_team = game.getTeam(self._player.getTeamID())
					
					self._client_socket.sendData("PASS %s" % (player_name))
					self._player_name = player_name
					game.connectPlayer(self._player_name, self)
					
					self._client_socket.sendData("%f %f" % game.getVariables())
					
					self._client_socket.sendData('\n'.join(game.getMap()))
					self._client_socket.sendData('\n'.join(["SPAC none %i,%i,%i" % space for space in player_team.getSpawnPoints()]))
					self._client_socket.sendData("LEND")
					
					self._client_socket.sendData('\n'.join([player.describe() for player in self._game.getPlayers()]))
					self._client_socket.sendData("LEND")
					
					self._client_socket.readData(True) #Wait for SYNC.
					
					time_1 = time.time()
					self._client_socket.sendData("PING")
					self._client_socket.readData(True) #Wait for PONG.
					time_1 = time.time() - time_1
					time_2 = time.time()
					self._client_socket.sendData("PING")
					self._client_socket.readData(True) #Wait for PONG.
					time_2 = time.time() - time_2
					time_3 = time.time()
					self._client_socket.sendData("PING")
					self._client_socket.readData(True) #Wait for PONG.
					time_3 = time.time() - time_3
					self._data_box.getLines() #Clear the queue.
					
					self._throttle = (time_1 + time_2 + time_3) / 3.0
					average_delay = int(self._throttle * 1000.0)
					
					(rank, score) = player_team.getStatus()
					duration = self._game.getDuration()
					current_time = self._game.getTime()
					self._client_socket.sendData("%i %i %i %i %i" % (current_time + average_delay, duration, rank, score, average_delay))
		except:
			game.disconnectPlayer(self._player_name)
			
	def kill(self):
		"""
		Indicates that this connection should terminate gracefully.
		"""
		self._alive = False
		
	def pushEvent(self, event):
		"""
		Queues an event for delivery to the client.
		
		@type event: basestring
		@param event: A non-newline-terminated protocol event.
		"""
		self._lock.acquire()
		self._queue.append(event)
		self._lock.release()
		
	def pushMissileEvent(self, source, id, data):
		"""
		Queues a missile position event for delivery to the client.
		
		If the missile identified already has an entry in the queue of data to
		be sent to the client, the old entry is replaced.
		
		@type source: basestring
		@param source: The name of the player who sent this event.
		@type id: int
		@param id: The ID of the missile being described.
		@type data: basestring
		@param data: The description of the missile.
		"""
		self._missiles_updates[(source, id)] = data
		
	def pushPlayerEvent(self, source, data):
		"""
		Queues a player position event for delivery to the client.
		
		If the player identified already has an entry in the queue of data to
		be sent to the client, the old entry is replaced.
		
		@type source: basestring
		@param source: The name of the player who sent this event.
		@type data: basestring
		@param data: The description of the player.
		"""
		self._players_updates[source] = data
		
	def run(self):
		"""
		Continually reads and processes data from the client, and delivers any
		queued data, sending player position data, missile position data, and
		general event data, in that order.
		"""
		readData = self._client_socket.readData
		sendData = self._client_socket.sendData
		data_box = self._data_box
		lock = self._lock
		queue = self._queue
		game = self._game
		player_name = self._player_name
		throttle = self._throttle
		
		try:
			while self._alive:
				readData()
				while data_box.hasLines():
					game.processLine(data_box.getLine(), player_name)
					
				outbound_data = None
				lock.acquire()
				try:
					if self._players_updates:
						sendData('\n'.join(self._players_updates.values()))
						self._players_updates = {}
					if self._missiles_updates:
						sendData('\n'.join(self._missiles_updates.values()))
						self._missiles_updates = {}
					if queue:
						sendData('\n'.join(queue))
						self._queue = queue = []
				finally:
					lock.release()
					
				time.sleep(throttle)
		except communication.ConnectionError:
			game.disconnectPlayer(player_name)
		finally:
			self._client_socket.kill()
			
			
class _Player(object):
	"""
	A container that locally represents a player in the game.
	"""
	_alive = False #: True while this player is alive and able to generate events.
	_bolts = None #: A collection of bolt identifiers.
	_bolts_fired = 0 #: The number of bolts fired by this player.
	_bolts_landed = 0 #: The number of bolts that connected.
	_bolts_modifiers = None #: A collection of bolt modifiers.
	_colour = None #: The colour of the player's ship.
	_deaths = 0 #: The number of times this player died.
	_initial_spawn_point = None #: The position at which this player will initially appear.
	_kills = 0 #: The number of kills made by this player.
	_last_alive = 0 #: The timestamp before which events concerning this player should be discarded.
	_lock = None #: A lock used for thread control.
	_missiles = None #: A collection of missile identifiers.
	_name = None #: The player's name.
	_power_source = None #: The player's power source.
	_ship_model = None #: The model of the player's ship.
	_suicides = 0 #: The number of times this player committed suicide.
	_team_id = None #: The ID of the team to which this player belongs.
	
	def __init__(self, name, team_id, colour, ship_model, bolts, bolts_modifiers, missiles, power_source, initial_spawn_point):
		"""
		Creates a representation of a player in the game.
		
		@type name: basestring
		@param name: The name of the player to add.
		@type team_id: int
		@param team: The ID of the team to which the player will belong.
		@type colour: sequence(3)
		@param colour: The (r, g, b) values of this player, as integers
		    from 0-1000.
		@type ship_model: basestring
		@param ship_model: The ID of the player's ship.
		@type bolts: sequence
		@param bolts: A collection of bolt IDs, specifying the player's
		    loadout.
		@type bolts_modifiers: sequence
		@param bolts_modifiers: A collection of modifier IDs, specifying
		    the player's bolt modifiers.
		@type missiles: sequence
		@param missiles: A collection of missile IDs, specifying the
		    player's loadout.
		@type power_source: int
		@param power_source: A power source ID, specifying the type of
		    energy that powers the player's ship.
		@type initial_spawn_point: sequence(3)
		@param initial_spawn_point: The (x, y, z) co-ordinates at which this
		    player will initially appear.
		"""
		self._name = name
		self._team_id = team_id
		self._colour = colour
		self._ship_model = ship_model
		self._bolts = bolts
		self._bolts_modifiers = bolts_modifiers
		self._missiles = missiles
		self._power_source = power_source
		self._initial_spawn_point = initial_spawn_point
		self._lock = threading.Lock()
		
	def __cmp__(self, player):
		"""
		Compares two players, allowing for sorting.
		
		@type player: L{_Player}
		@param player: The player to which this will be compared.
		
		@rtype: int
		@return: 1 if the other player has fewer kills; -1 if the other player
		    has more; 0 if both scores are equal.
		"""
		if player.getKills() < self._kills:
			return 1
		elif player.getKills() > self._kills:
			return -1
		return 0
		
	def addKill(self):
		"""
		Credits this player with a kill.
		"""
		self._lock.acquire()
		self._kills += 1
		self._lock.release()
		
	def describe(self):
		"""
		Returns a protocol-event description of this player.
		
		@rtype: str
		@return: A protocol-event description of this player.
		"""
		bolt_1 = bolt_2 = bolt_3 = bolt_4 = "__"
		modifier_1 = modifier_2 = modifier_3 = modifier_4 = 0
		if len(self._bolts) >= 1:
			bolt_1 = self._bolts[0]
			modifier_1 = self._bolts_modifiers[0]
		if len(self._bolts) >= 2:
			bolt_2 = self._bolts[1]
			modifier_2 = self._bolts_modifiers[1]
		if len(self._bolts) >= 3:
			bolt_3 = self._bolts[2]
			modifier_3 = self._bolts_modifiers[2]
		if len(self._bolts) == 4:
			bolt_4 = self._bolts[3]
			modifier_4 = self._bolts_modifiers[3]
			
		missile_1 = missile_2 = missile_3 = "__"
		if len(self._missiles) >= 1:
			missile_1 = self._missiles[0]
		if len(self._missiles) >= 2:
			missile_2 = self._missiles[1]
		if len(self._missiles) == 3:
			missile_3 = self._missiles[2]
			
		(r, g, b) = [int(v * 1000) for v in self._colour]
		(x, y, z) = self._initial_spawn_point
		return "PLYR %s %i %i,%i,%i %s %s,%s,%s,%s %i,%i,%i,%i %s,%s,%s %i %i,%i,%i" % (
		 self._name,
		 self._team_id,
		 r, g, b,
		 self._ship_model,
		 bolt_1, bolt_2, bolt_3, bolt_4,
		 modifier_1, modifier_2, modifier_3, modifier_4,
		 missile_1, missile_2, missile_3,
		 self._power_source,
		 x, y, z
		)
		
	def fireBolt(self):
		"""
		Increments the number of bolts fired by this player.
		
		Note: Only one thread will ever access this function, so locking is not
		necessary.
		"""
		self._bolts_fired += 1
		
	def getKills(self):
		"""
		Returns the number of kills credited to this player.
		
		@rtype: int
		@return: The number of kills credited to this player.
		"""
		return self._kills
		
	def getLastAlive(self):
		"""
		Returns the timestamp at which this player was last reported alive, used
		to discard conflicting damage/kill events.
		
		@rtype: int
		@return: The timestamp, in milliseconds, at which this player was last
		    reported as being alive.
		"""
		self._lock.acquire()
		try:
			return self._last_alive
		finally:
			self._lock.release()
			
	def getName(self):
		"""
		Returns the name of this player.
		
		@rtype: str
		@return: The name of this player.
		"""
		return self._name
		
	def getStats(self):
		"""
		Returns the post-game statistical data associated with this player.
		
		@rtype: tuple(4)
		@return: The player's accuracy, as a string, and the player's kills,
		    deaths, and suicides.
		"""
		return ("%.2f%%" % (100.0 * self._bolts_landed / float(self._bolts_fired or 1)), self._kills, self._deaths, self._suicides)
		
	def getTeamID(self):
		"""
		Returns the ID of the team to which this player belongs.
		
		@rtype: int
		@return: The ID of the team to which this player belongs.
		"""
		return self._team_id
		
	def isAlive(self):
		"""
		Indicates whether this player is currently alive, in the game.
		
		@rtype: bool
		@return: True if this player is currently alive.
		"""
		self._lock.acquire()
		try:
			return self._alive
		finally:
			self._lock.release()
			
	def kill(self, suicide=False):
		"""
		Kills this player, incrementing its death count and, if appropriate, its
		suicide count.
		
		@type suicide: bool
		@param suicide: True if this death was a suicide.
		"""
		self._lock.acquire()
		self._alive = False
		self._deaths += 1
		if suicide:
			self._suicides += 1
		self._lock.release()
		
	def landBolt(self):
		"""
		Increments the number of bolts landed by this player.
		
		Note: Only one thread will ever access this function, so locking is not
		necessary.
		"""
		self._bolts_landed += 1
		
	def revive(self, timestamp):
		"""
		Marks this player as being alive and updates its last-alive timestamp.
		
		@type timestamp: int
		@param timestamp: The current game-timestamp, expressed in milliseconds.
		"""
		self._lock.acquire()
		self._alive = True
		self._last_alive = timestamp
		self._lock.release()
		
class _Team(object):
	"""
	A container that locally represents a team in the game.
	"""
	_lock = None #: A lock used for thread control.
	_members = None #: The players who comprise this team.
	_rank = 0 #: This team's rank.
	_score = 0 #: This team's score.
	_spawn_points = None #: A list of all spawn points intended for use by this team.
	
	def __init__(self, spawn_points):
		"""
		Initialises this team, settings its membership to none.
		
		@type spawn_points: sequence
		@param spawn_points: A list of all spawn points that this team's members
		    may use.
		"""
		self._spawn_points = spawn_points
		self._members = []
		self._lock = threading.Lock()
		
	def addMember(self, player):
		"""
		Adds a player to this team.
		
		@type player: L{_Player}
		@param player: The player to be added.
		"""
		self._lock.acquire()
		if not player in self._members:
			self._members.append(player)
		self._lock.release()
		
	def getMembers(self):
		"""
		Returns a list of all players that comprise this team.
		
		@rtype: list
		@return: A list of all L{_Player}s that comprise this team.
		"""
		self._lock.acquire()
		try:
			return self._members[:]
		finally:
			self._lock.release()
			
	def getRank(self):
		"""
		Returns this team's rank, relative to all other teams.
		
		@rtype: int
		@return: This team's rank, 1-indexed.
		"""
		self._lock.acquire()
		try:
			return self._rank
		finally:
			self._lock.release()
			
	def getScore(self):
		"""
		Returns this team's score.
		
		@rtype: int
		@return: This team's score.
		"""
		self._lock.acquire()
		try:
			return self._score
		finally:
			self._lock.release()
			
	def getSpawnPoints(self):
		"""
		Returns all spawn-points available for use by this team's members.
		
		@rtype: sequence
		@return: A collection of (x, y, z) spawn-points.
		"""
		return self._spawn_points
		
	def getStatus(self):
		"""
		Returns this team's rank and score.
		
		@rtype: tuple(2)
		@return: This team's rank and score.
		"""
		self._lock.acquire()
		try:
			return (self._rank, self._score)
		finally:
			self._lock.release()
			
	def removeMember(self, player):
		"""
		Removes a player from this team.
		
		@type player: L{_Player}
		@param player: The player to be removed.
		"""
		self._lock.acquire()
		if player in self._members:
			self._members.remove(player)
		self._lock.release()
		
	def setRank(self, rank):
		"""
		Sets this team's rank.
		
		@type rank: int
		@param rank: This team's rank, 1-indexed.
		"""
		self._lock.acquire()
		self._rank = rank
		self._lock.release()
		
	def setScore(self, increment):
		"""
		Sets this team's score, based on an offset of its current score.
		
		@type increment: int
		@param increment: The number of points to add (or subtract) from this
		    team's score.
		"""
		self._lock.acquire()
		try:
			self._score += increment
			return self._score
		finally:
			self._lock.release()
			